简介
CVE-2020-17518: 文件写入漏洞
攻击者利用REST API,可以修改HTTP头,将上传的文件写入到本地文件系统上的任意位置(Flink 1.5.1进程能访问到的),网上给出的上传是/jars/upload
,上传并不限于这个路径,任意路径都可触发上传操作,后面会说明原理。
commit:
https://github.com/apache/flink/commit/a5264a6f41524afe8ceadf1d8ddc8c80f323ebc4
diff:
poc
1 | POST /xxxxx HTTP/1.1 |
CVE-2020-17519: 文件读取漏洞
Apache Flink 1.11.0 允许攻击者通过JobManager进程的REST API读取JobManager本地文件系统上的任何文件(JobManager进程能访问到的)。
commit:
https://github.com/apache/flink/commit/b561010b0ee741543c3953306037f00d7a9f0801
diff:
poc
1 | /jobmanager/logs/..%252fREADME.txt |
前置知识
一个 Flink 集群总是包含一个 JobManager 以及一个或多个 Flink TaskManager。JobManager 负责处理 Job 提交、 Job 监控以及资源管理。Flink TaskManager 运行 worker 进程, 负责实际任务 Tasks 的执行,而这些任务共同组成了一个 Flink Job。
REST-APi
Flink
具有监控 API
,可用于查询正在运行的作业以及最近完成的作业的状态和统计信息。该监控 API
被用于 Flink
自己的仪表盘,同时也可用于自定义监控工具。
https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/ops/rest_api.html#rest-api
REST API
后端位于 flink-runtime
项目中。核心类是 org.apache.flink.runtime.webmonitor.WebMonitorEndpoint
,用来配置服务器和请求路由。
Netty
Rest API
里请求的分派就涉及到了使用 Netty
和 Netty Router
库。
Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架。
Channel
Channel,表示一个连接,可以理解为每一个请求,就是一个Channel
ChannelHandler,核心处理业务就在这里,用于处理业务请求
ChannelHandlerContext,用于传输业务数据,flink
中的例子对应AbstractChannelHandlerContext
,包含prev
上一节点 handler
和 next
下一节点 handler
。
ChannelPipeline
,用于保存处理过程需要用到的 ChannelHandler
和ChannelHandlerContext
。
事件在 pipeline
中的传播:
- 传播行为:事件执行到某个
Handler
后,如果不手动触发ctx.fireChannelRead
,则传播中断。 - 执行线程:业务线程默认是在
NioEventLoop
中执行。如果业务处理有阻塞,需要考虑另起线程执行。
事件的处理:主要是在 Handler
的 channelRead
上进行处理。
分析
路由
初始化
REST API
后端位于 flink-runtime
项目中。核心类是org.apache.flink.runtime.webmonitor.WebMonitorEndpoint
,用来配置服务器和请求路由。
根据官方文档,可以知道,如果添加一个新的 api
需要:
- 添加一个新的
MessageHeaders
实现类,作为新请求的接口。 - 添加一个新的
AbstractRestHandler
实现类,相当于一个ChannelHandler
,该类接收并处理MessageHeaders
类的请求。 - 将处理程序添加到
org.apache.flink.runtime.webmonitor.WebMonitorEndpoint#initializeHandlers()
中。
以出现漏洞的api
为例,在初始化路由的时候,代码如下:
1 | protected List<Tuple2<RestHandlerSpecification, ChannelInboundHandler>> initializeHandlers(CompletableFuture<String> localAddressFuture) { |
这里的 JobManagerCustomLogHandler
就是AbstractRestHandler
的实现类,也就是体现 api
具体功能的类,而 JobManagerCustomLogHeaders
是 MessageHeaders
类的实现类,具体定义了 api
的访问路径,这里的 JobManagerCustomLogHeaders.getInstance()
体现了单例的设计模式:
1 | public class JobManagerCustomLogHeaders implements UntypedResponseMessageHeaders<EmptyRequestBody, FileMessageParameters> { |
注册路由
之后,将初始化的路由进行注册,主要调用的是RestServerEndpoint#registerHandler
,这里会根据MessageHeaders
实现类给出的请求方法分派给Router
类的不同adder
处理。
1 | private static void registerHandler(Router router, String handlerURL, HttpMethodWrapper httpMethod, ChannelInboundHandler handler) { |
注册之后,保存在routers里,不同的处理方式存放不同的handler。
请求分派
之后,当我们发起请求的时候,就由RouterHandler#channelRead0
来进行请求的分派。
根据我们发起的请求,获取请求方式,然后获取url
,此时的url
没有解码,然后实例化一个QueryStringDecoder
,赋值给qsd
。
然后,就调用 Router#route
,传入三个参数,请求方式、qsd#path
、qsd#parameters
。
第一次解码
path是一开始没有被赋值的,于是,这里会调用decodeComponent
方法,并且传入我们请求的url
,也就是/jobmanager/logs/..%252f..%252f..%252f..%252f..%252f..%252fetc%252fpasswd
decodeComponent
是自定义的一个解码方法,在这里会定位到%
,并解码 %xx
,然后拼接,问题就在于这里的解码只解了一次,于是返回的path
为/jobmanager/logs/..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd
。
然后调用route
方法。
第二次解码
根据请求的method
,获取routers
里对应的路由。然后对path
调用decodePathTokens
。
这里会以/
拆分path
,然后分别解码,其中,解码依然是调用了decodeComponent
这个方法进行解码,
最终解码的结果为,赋值给tokens
路径匹配
然后继续调用另一个route
方法,传入path
和tokens
,这个方法里会根据传入的 path
在 router
里进行匹配对应的路径,我们匹配到的是 jobmanager/logs/:filename
。
然后获取到对应的handler
->JobManagerCustomLogHandler
。
然后将tokens
、handler
、等存到RoutedRequest
里。
CVE-2020-17519
之后,请求/事件会被传播到LeaderRetrievalHandler#channelRead0
,这里,会调用之前匹配到的 Handler
的 respondAsLeader
方法。
而 Log
对应的 JobManagerCustomLogHandler
没有 respondAsLeader
方法,于是调用其父类 AbstractHandler
的 respondAsLeader
方法。接着,在内部又调了AbstractHandler
子类 AbstractJobManagerFileHandler
的 respondAsLeader
。
调用JobManagerCustomLogHandler#getFile
。
从 handlerRequest
里获取到 tokens
里的 filename
。
CVE-2020-17518
上传的处理是在 FileUploadHandler#channelRead0
,产生的原因依然是存在路径遍历,可以上传到任意目录下,造成任意文件写入。
当 msg
是一个 HttpContent
类型的时候,可以走到 FileUploadHandler
的上传逻辑,于是构造一个上传表单即可,具体的包在文首有。
这里上传请求的 url
路径可以是任意的,因为请求是一定会分派到 FileUploadHandler
进行处理的,这是由 flink
设置的 handler
处理链所决定的。
文件是通过 renameTo
方法进行移动的,将上传的临时文件进行转移。
在offer
方法里会对body
进行解析,解析出 filename
等信息。
调用链较长
随后在renameTo
发生遍历。
总结
任意文件读取主要是由于在处理访问logs请求的时候,Handler会去请求读取logs文件,然而该文件名的编码处理出现了问题,从而在读文件的时候造成了路径遍历,文件上传也是同样的道理,不过有意思的是,任意路径都可以触发,即使路径并不存在,因为处理文件的Handler是众多对请求进行处理的必经之路。